package dwarffortress.loaders;

import dwarffortress.io.DFInputStream;
import dwarffortress.analysis.StructureAnalyzer;
import dwarffortress.analysis.ObjectAnalyzer;
import dwarffortress.annotations.FixedValue;
import dwarffortress.annotations.SpecialLength;
import dwarffortress.annotations.FixedLength;
import dwarffortress.annotations.FieldRef;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Array;

public class ReflectiveLoader {
    public <T> T loadObject(Class<T> clazz, DFInputStream in) throws IOException, IllegalAccessException, InstantiationException {
        StructureAnalyzer<T> classAnalyzer = new StructureAnalyzer<T>(clazz);
        T result = clazz.newInstance();
        ObjectAnalyzer<T> analyzer = classAnalyzer.analyze(result);
        for (int i = 0; i < analyzer.getFieldCount(); i++) {
            if (analyzer.isLoadable(i)) {
                Field field = classAnalyzer.getField(i);
                Class<?> fieldType = field.getType();

                if (fieldType.isPrimitive()) {
                    int fieldSize = 0;
                    if (Integer.TYPE.equals(fieldType)) {
                        analyzer.setIntValue(i, in.readInt());
                        fieldSize = 4;
                    } else if (Short.TYPE.equals(fieldType)) {
                        analyzer.setShortValue(i, in.readShort());
                        fieldSize = 2;
                    } else if (Byte.TYPE.equals(fieldType)) {
                        analyzer.setByteValue(i, in.readByte());
                        fieldSize = 1;
                    } else {
                        throw new UnsupportedOperationException("Not implemented yet");
                    }

                    if (null != field.getAnnotation(FixedValue.class)) {
                        FixedValue value = field.getAnnotation(FixedValue.class);
                        if (value.essential()) {
                            if (analyzer.getIntValue(i) != value.value()) {
                                int offset = in.getOffset() - fieldSize;
                                throw new StrongAssumptionException(String.format("Assumption failed while reading field #%d in structure %s at position %d (%x) : expected %d got %d", i, clazz.getSimpleName(), offset, offset, value.value(), analyzer.getIntValue(i)));
                            }
                        }
                    }
                } else { //Field is String/Array/Object
                    if (String.class.equals(fieldType)) { //Field is String
                        analyzer.setValue(i, in.readDFString());
                    } else if (analyzer.isArray(i)) { //Arrays
                        int length;
                        if (null == field.getAnnotation(SpecialLength.class)) {
                            length = in.readInt();
                        } else {
                            SpecialLength specialLength = field.getAnnotation(SpecialLength.class);
                            int field_num = -1;
                            switch (specialLength.fieldRef()) {
                                case OFFSET:
                                    field_num = i + specialLength.offset();
                                    break;
                                case NUMBER:
                                    field_num = specialLength.fieldNum();
                                    break;
                                case NAME:
                                    field_num = classAnalyzer.getFieldIndexForName(specialLength.fieldName());
                            }
                            if (!specialLength.fieldRef().equals(FieldRef.FIXED)) {
                                if (classAnalyzer.isArray(field_num)) {
                                    length = analyzer.getArrayLength(field_num);
                                } else {
                                    length = analyzer.getIntValue(field_num);
                                }
                            } else {
                                length = specialLength.fixedSize();
                            }
                        }
                        if (null != field.getAnnotation(FixedLength.class)) {
                            FixedLength fixedLength = field.getAnnotation(FixedLength.class);
                            if (length != fixedLength.length()) {
                                throw new IllegalStateException("Fixed length does not match given length for " + field.getName() + " got length " + length);
                            }
                        }

                        Class arrayType = classAnalyzer.getField(i).getType().getComponentType();
                        Object fieldValue;
                        if (arrayType.isPrimitive()) {
                            in.checkArrayLength(length);
                            if (Integer.TYPE.equals(arrayType)) {
                                int[] value = new int[length];
                                for (int j = 0; j < value.length; j++) {
                                    value[j] = in.readInt();
                                }
                                fieldValue = value;
                            } else if (Short.TYPE.equals(arrayType)) {
                                short[] value = new short[length];
                                for (int j = 0; j < value.length; j++) {
                                    value[j] = in.readShort();
                                }
                                fieldValue = value;
                            } else if (Byte.TYPE.equals(arrayType)) {
                                byte[] value = new byte[length];
                                for (int j = 0; j < value.length; j++) {
                                    value[j] = in.readByte();
                                }
                                fieldValue = value;
                            } else {
                                throw new UnsupportedOperationException("Not implemented yet");
                            }
                        } else { //Array of non-primitives
                            if (String.class.equals(arrayType)) {
                                String[] value = new String[length];
                                for (int j = 0; j < value.length; j++) {
                                    value[j] = in.readDFString();
                                }
                                fieldValue = value;
                            } else {
                                Object values = Array.newInstance(arrayType, length);
                                for (int j = 0; j < length; j++) {
                                    Array.set(values, j, loadObject(arrayType, in));
                                }
                                fieldValue = values;
                            }
                        }
                        analyzer.setValue(i, fieldValue);
                    } else { //Just object
                        Object o = loadObject(fieldType, in);
                        analyzer.setValue(i, o);
                    }
                }

            }
        }
        return result;
    }


}
